﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.DirectoryServices;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    #endregion

    /// <summary>
    /// Base class for DNS records
    /// </summary>
    [Serializable]
    public class DNSResourceRecord : ADBaseObjectProperties {

        #region Members
        public static readonly new string[] AttributesToGet;

        /// <summary>
        /// An unsigned binary integer containing the length, in bytes, of the Data field.
        /// </summary>
        public ushort DataLength { get; private set; }

        /// <summary>
        /// Indicates if a DNS record has been marked for deletion.
        /// This attribute exists to make searching for tombstoned records easier and faster. 
        /// Tombstoned objects are objects that have been deleted but not yet removed from the directory.
        /// </summary>
        /// <remarks>
        /// The clean-up of dsnTombstoned records is performed by the DNS server process.
        /// Everyday at 2AM (non-configurable) the DNS server scans all DNS integrated zones in AD and determines 
        /// if the tombstoned record is ready to be deleted. The default retention time of the tombstoned records is 7 days. 
        /// This value can be changed by the DsTombStoneinterval value (dnscmd w2k8r2dc01 /config /DsTombstoneInterval value) 
        /// or by editing the registry under HKLM\CCS\Services\DNS\Parameters Value Name:DsTombstoneInterval Value Type: DWORD). 
        /// The value is in seconds.
        /// If the client performs an update to a record, even though the record has already been scavenged, but is still in the 
        /// Directory and wasn’t deleted completely, the update is accepted and the dnsTombstoned record is changed back to not set, 
        /// meaning the record will retain the same GUID and will be loaded by DNS.
        /// </remarks>
        public bool DNSTombstoned { get; private set; }

        /// <summary>
        /// Not used. The value MUST be 0x0000.
        /// </summary>
        public ushort Flags { get; private set; }

        /// <summary>
        /// The least-significant byte of one of the RANK* flag values. See dwFlags (section 2.2.2.2.5).
        /// </summary>
        public DNSRecordRank Rank { get; private set; }

        public DNSRecordClass RecordClass = DNSRecordClass.IN;

        /// <summary>
        /// The resource record's type. See DNS_RECORD_TYPE (section 2.2.2.1.1).
        /// </summary>
        public DNSRecordType RecordType { get; private set; }

        /// <summary>
        /// The string representation of the data block
        /// </summary>
        public string RecordData { get; protected set; }

        /// <summary>
        /// Not used.  Reservied for future use.
        /// </summary>
        public uint Reserved { get; private set; }

        /// <summary>
        /// The time the record was last updated, or Static for non-dynamic records
        /// </summary>
        public string TimeStampUTC { get; private set; }

        /// <summary>
        /// The time-to-liee for the record, in seconds
        /// </summary>
        public uint TTLSeconds { get; private set; }

        /// <summary>
        /// The time-to-liee for the record
        /// </summary>
        public TimeSpan TTLExpiry { get; private set; }

        /// <summary>
        /// The serial number of the SOA record of the zone containing this resource record. See DNS_RPC_RECORD_SOA (section 2.2.2.2.4.3).
        /// </summary>
        public uint UpdatedAtSerial { get; private set; }

        /// <summary>
        /// The version number associated with the resource record attribute. The value MUST be 0x05.
        /// </summary>
        public byte Version { get; private set; }

        #region Collections
        /// <summary>
        /// https://msdn.microsoft.com/en-us/library/ee898781.aspx
        /// The dnsRecord attribute is used to store DNS resource record definitions. This attribute MUST be formatted as follows:
        /// 
        /// DataLength (2 bytes): An unsigned binary integer containing the length, in bytes, of the Data field.
        /// Type (2 bytes): The resource record's type. See DNS_RECORD_TYPE (section 2.2.2.1.1).
        /// Version (1 byte): The version number associated with the resource record attribute. The value MUST be 0x05.
        /// Rank (1 byte): The least-significant byte of one of the RANK* flag values. See dwFlags (section 2.2.2.2.5).
        /// Flags (2 bytes): Not used. The value MUST be 0x0000.
        /// Serial (4 bytes): The serial number of the SOA record of the zone containing this resource record. See DNS_RPC_RECORD_SOA (section 2.2.2.2.4.3).
        /// TtlSeconds (4 bytes): See dwTtlSeconds (section 2.2.2.2.5). This field uses big-endian byte order.
        /// Reserved (4 bytes): This field is reserved for future use. The value MUST be 0x00000000.
        /// TimeStamp (4 bytes): See dwTimeStamp (section 2.2.2.2.5).
        /// Data (variable): The resource record's data. See DNS_RPC_RECORD_DATA (section 2.2.2.2.4).
        /// </summary>
        /// <example>
        ///                                 1  1  1  1  1  1
        ///   0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |                 DATA LENGTH                   |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |                      TYPE                     |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |        VERSION        |         RANK          |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |                     FLAGS                     |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |                     SERIAL                    |
        ///  |                                               |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |                  TTLSECONDS                   |
        ///  |                                               |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |                    RESERVED                   |
        ///  |                                               |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |                   TIMESTAMP                   |
        ///  |                                               |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
        ///  /                     DATA                      /
        /// /                                               /
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  </example>
        public IReadOnlyList<byte[]> DNSRecords { get; private set; }

        public byte[] DNSRecord { get; private set; }

        public IReadOnlyList<DNSResourceRecord> ChildDNSRecords { get; private set; } 
        #endregion

        #region ObjectCategory/ObjectClass identifiers
        public static readonly string ObjectCategoryShortName = "dnsNode";
        public static readonly string ObjectClassShortName = "dnsNode";
        #endregion
        #endregion

        #region Constructor
        static DNSResourceRecord() {
            var attributesToGet = new List<string>();

            attributesToGet.AddRange(new string[]
            {
                "dnsRecord",
                "dnsTombstoned"
            });

            attributesToGet.AddRange(ADBaseObjectProperties.AttributesToGet);

            AttributesToGet = attributesToGet.Distinct().ToArray();
            attributesToGet = null;
        }

        public DNSResourceRecord() {
            this.Initialize();
        }

        public DNSResourceRecord(DirectoryEntry directoryEntry, bool destroyInputObject = true)
            : base(directoryEntry, destroyInputObject) {
            if (this.GetType().Name == typeof(DNSResourceRecord).Name) {
                this.Attributes = null;
            }
        }

        public DNSResourceRecord(SearchResult result, bool destroyInputObject = true)
            : base(result, destroyInputObject) {
            if (this.GetType().Name == typeof(DNSResourceRecord).Name) {
                this.Attributes = null;
            }
        }
        #endregion

        #region Methods
        protected override void Initialize() {
            if (string.IsNullOrWhiteSpace(this.DN)) {
                base.Initialize();
            }

            this.DataLength = 0;
            this.Rank = DNSRecordRank.Undefined;
            this.RecordData = string.Empty;
            this.RecordType = DNSRecordType.Undefined;
            this.TimeStampUTC = string.Empty;
            this.TTLSeconds = 0;
            this.UpdatedAtSerial = 0;

            this.DNSRecords = new List<byte[]>();
            this.ChildDNSRecords = new List<DNSResourceRecord>();
        }

        protected override void ProcessResults() {

            try {
                base.ProcessResults();

                if (this.Attributes.ContainsKey("dnsRecord")) {
                    var dnsRecords = new List<byte[]>();
                    for (int index = 0; index < this.Attributes["dnsRecord"].Length; index++) {
                        if (this.Attributes["dnsRecord"][index] as byte[] != null) {
                            var bytes = (byte[])this.Attributes["dnsRecord"][index];
                            dnsRecords.Add(bytes);
                        }
                    }
                    this.DNSRecords = dnsRecords;
                }

                // If there are multiple records, use the first record as the parent.
                var childDNSRecords = new List<DNSResourceRecord>();
                for (int index = 0; index < this.DNSRecords.Count; index++) {
                    var dnsRecord = this.DNSRecords[index];
                    if (index == 0) {
                        this.DNSRecord = dnsRecord;
                        this.ProcessDNSRecord(dnsRecord);
                    }
                    else {
                        DNSResourceRecord childDNSRecord = base.Clone() as DNSResourceRecord;
                        childDNSRecord.Initialize();
                        childDNSRecord.DNSRecord = dnsRecord;
                        childDNSRecord.ProcessDNSRecord(dnsRecord);
                        childDNSRecords.Add(childDNSRecord);
                    }
                }
                this.ChildDNSRecords = childDNSRecords;

                this.IsValid = !string.IsNullOrWhiteSpace(this.DN) &&
                    this.ObjectClass.Any(x => x.Equals(ObjectCategoryShortName, StringComparison.OrdinalIgnoreCase));
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} Error: {3}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                throw;
            }

        }

        public void ProcessDNSRecord(byte[] dnsRecord) {
            var memoryStream = new MemoryStream(dnsRecord);
            using (var propertyReader = new EndianBinaryReader(memoryStream)) {
                this.ProcessDNSRecordStream(propertyReader);
            }
        }

        internal virtual void ProcessDNSRecordStream(EndianBinaryReader propertyReader) {
            this.DataLength = propertyReader.ReadUInt16();
            this.RecordType = (DNSRecordType)propertyReader.ReadUInt16();
            this.Version = propertyReader.ReadByte();
            this.Rank = (DNSRecordRank)propertyReader.ReadByte();
            this.Flags = propertyReader.ReadUInt16();
            this.UpdatedAtSerial = propertyReader.ReadUInt32();
            this.TTLSeconds = propertyReader.ReadUInt32(isBigEndian: true);
            if (this.TTLSeconds > 0) {
                this.TTLExpiry = TimeSpan.FromSeconds(this.TTLSeconds);
            }
            this.Reserved = propertyReader.ReadUInt32();
            uint timeStamp = propertyReader.ReadUInt32();
            if (timeStamp > 0) {
                this.TimeStampUTC = WindowsEpochStart.AddHours(timeStamp).YMDHMSFriendly();
            }
            else {
                this.TimeStampUTC = "Static";
            }
        }

        /// <summary>
        /// Reads the name from the RData block
        /// </summary>
        /// <returns>The name</returns>
        /// <example>
        ///                                  1  1  1  1  1  1
        ///    0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |         LENGTH        |   NUMBER OF LABELS    |
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  |      LABEL LENGTH     |                       |
        ///  |--+--+--+--+--+--+--+--+                       |
        ///  /                     DATA                      /
        ///  /                                               /
        ///  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        ///  </example>
        internal string ReadName(EndianBinaryReader propertyReader) {

            string name = string.Empty;

            byte length = propertyReader.ReadByte();
            byte labelCount = propertyReader.ReadByte();

            string[] labels = new string[labelCount];

            for (int index = 0; index < labelCount; index++) {
                byte labelLength = propertyReader.ReadByte();
                labels[index] = new string(propertyReader.ReadChars(labelLength));
            }

            // Drop the terminating character
            propertyReader.ReadByte();
            name = string.Join(".", labels);

            return name;
        }

        public override string ToString() {
            return string.Format("{0} {1} IN {2} {3}", this.Name, this.TTLSeconds, this.RecordType, this.RecordData);
        }
        #endregion
    }
}
